/**
* © 2016 Telenav, Inc. All Rights Reserved
* <p/>
* Licensed under the Apache License, Version 2.0 (the "License").
* <p/>
* You may not use this file except in compliance with the License. You may obtain a copy of the
* License at http://www.apache.org/licenses/LICENSE-2.0. Unless required by applicable law or
* agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for
* the specific language governing permissions and limitations under the License.
*/
package com.telenav.expandablepager;
import android.animation.Animator;
import android.animation.ObjectAnimator;
import android.animation.ValueAnimator;
import android.content.Context;
import android.support.annotation.IntDef;
import android.support.v4.view.MotionEventCompat;
import android.support.v4.view.animation.FastOutSlowInInterpolator;
import android.util.AttributeSet;
import android.view.MotionEvent;
import android.view.View;
import android.view.animation.Interpolator;
import android.view.animation.LinearInterpolator;
import android.widget.RelativeLayout;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.Iterator;
import java.util.List;
import java.util.SortedSet;
import java.util.TreeSet;
import com.telenav.expandablepager.listener.OnSlideListener;
/**
* Container that slides vertically between provided slide values.
*/
public class SlidingContainer extends RelativeLayout {
public static final int STATE_COLLAPSED = 0, STATE_EXPANDED = 1, STATE_HIDDEN = -1;
private static final int SLIDE_THRESHOLD_DIPS = 20;
private final float DEFAULT_SLIDE_THRESHOLD;
/**
* Each slide event must pass this threshold in order not to be ignored.
*/
private float slideThreshold;
private int viewHeight;
/**
* The container will stop sliding when it reaches one of the stopValues.
*/
private List<Float> stopValues = new ArrayList<>();
private int stopValueIndex = 0;
private float startYCoordinate;
/**
* Difference between start touch Y coordinate and current Y coordinate.
*/
private float touchDelta;
/**
* Current translationY value
*/
private float translated = 0;
private OnSlideListener slideListener;
private int duration = 200;
public SlidingContainer(Context context) {
super(context);
slideThreshold = context.getResources().getDisplayMetrics().density * SLIDE_THRESHOLD_DIPS;
DEFAULT_SLIDE_THRESHOLD = slideThreshold;
}
public SlidingContainer(Context context, AttributeSet attrs) {
super(context, attrs);
slideThreshold = context.getResources().getDisplayMetrics().density * SLIDE_THRESHOLD_DIPS;
DEFAULT_SLIDE_THRESHOLD = slideThreshold;
}
public void setSlideListener(OnSlideListener slideListener) {
this.slideListener = slideListener;
}
public void setAnimationDuration(int duration) {
this.duration = duration;
}
@Override
protected void onSizeChanged(int w, int h, int oldw, int oldh) {
super.onSizeChanged(w, h, oldw, oldh);
if (viewHeight == 0) {
viewHeight = h;
Iterator<Float> iter = stopValues.iterator();
while (iter.hasNext()) {
Float i = iter.next();
if (i >= viewHeight || i < 0)
iter.remove();
}
}
}
@Override
public boolean onInterceptTouchEvent(MotionEvent event) {
return !translate(event);
}
@Override
public boolean onTouchEvent(MotionEvent event) {
translate(event);
return true;
}
protected void enableSlide(boolean enable) {
slideThreshold = enable ? DEFAULT_SLIDE_THRESHOLD : Integer.MAX_VALUE;
}
private boolean translate(MotionEvent ev) {
final int action = MotionEventCompat.getActionMasked(ev);
int stepSize = stopValues.size();
switch (action) {
case MotionEvent.ACTION_DOWN: {
startYCoordinate = ev.getRawY();
translated = 0;
break;
}
case MotionEvent.ACTION_MOVE: {
touchDelta = (startYCoordinate - ev.getRawY());
if (Math.abs(touchDelta) > slideThreshold) {
float startingPointY, nextPointY, maxDiff, tempDelta, auxDelta = 0;
tempDelta = touchDelta + (touchDelta < 0 ? 1 : -1) * slideThreshold;
startingPointY = stopValues.get(stopValueIndex);
if (!isUpwardGesture() && stopValueIndex >= 1) {
nextPointY = stopValues.get(stopValueIndex - 1);
maxDiff = nextPointY - stopValues.get(stopValueIndex);
auxDelta = Math.min(-tempDelta, maxDiff);
} else if (isUpwardGesture() && stopValueIndex < stepSize - 1) {
nextPointY = stopValues.get(stopValueIndex + 1);
maxDiff = nextPointY - stopValues.get(stopValueIndex);
auxDelta = Math.max(-tempDelta, maxDiff);
}
float preTranslated = translated;
translated = startingPointY + auxDelta;
setTranslationY(translated);
if (preTranslated != translated)
notifySlideEvent(translated);
return false;
}
return true;
}
case MotionEvent.ACTION_UP: {
if (Math.abs(touchDelta) > slideThreshold) {
if (!isUpwardGesture() && stopValueIndex > 0)
stopValueIndex--;
else if (isUpwardGesture() && stopValueIndex < stepSize - 1)
stopValueIndex++;
if (!stopValues.contains(translated)) {
animate(stopValues.get(stopValueIndex));
} else
onSettled(stopValueIndex);
startYCoordinate = -1;
touchDelta = 0;
}
break;
}
case MotionEvent.ACTION_CANCEL: {
break;
}
case MotionEvent.ACTION_POINTER_UP: {
break;
}
}
return true;
}
protected void onSettled(int slideValueIndex) {
}
/**
* indicates that that the finger moved up
*/
private boolean isUpwardGesture() {
return touchDelta > 0;
}
/**
* Convenience method, uses FastOutSlowInInterpolator and default animation duration. See {@link SlidingContainer#animate(float, int, Interpolator)}
* @param amount translationY amount
*/
private void animate(float amount) {
animate(amount, duration, new LinearInterpolator());
}
/**
* Convenience method, uses FastOutSlowInInterpolator. See {@link SlidingContainer#animate(float, int, Interpolator)}
* @param amount translationY amount
* @param duration animation duration
*/
private void animate(float amount, int duration) {
animate(amount, duration, new FastOutSlowInInterpolator());
}
/**
* Animate translationY to the next stopValue
* @param amount translationY amount
* @param duration animation duration
* @param interpolator animation interpolator
*/
private void animate(final float amount, int duration, Interpolator interpolator) {
ObjectAnimator oa = ObjectAnimator.ofFloat(this, View.TRANSLATION_Y, amount)
.setDuration(duration);
oa.setInterpolator(interpolator);
oa.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
@Override
public void onAnimationUpdate(ValueAnimator animation) {
notifySlideEvent(Math.round(((Float) animation.getAnimatedValue())));
}
});
oa.addListener(new CustomAnimationListener() {
@Override
public void onAnimationEnd(Animator animator) {
onSettled(stopValueIndex);
}
});
oa.start();
}
public List<Float> getStopValues() {
return stopValues;
}
/**
* Stops sliding at the specified values.
* @param stopValues list of stop values
*/
public void setStopValues(Float... stopValues) {
SortedSet<Float> s = new TreeSet<>(Collections.reverseOrder());
s.addAll(Arrays.asList(stopValues));
this.stopValues.clear();
this.stopValues.addAll(s);
this.stopValues.add(0f);
stopValueIndex = 0;
}
public Float getCurrentStopValue() {
return stopValues.get(stopValueIndex);
}
/**
* Sets the container position to a given state. No animation occurs. For an animated alternative see {@link SlidingContainer#animateToState(int, int)}
*/
public boolean setState(@SliderState int state) {
if (!stopValues.isEmpty())
switch (state) {
case STATE_COLLAPSED:
setTranslationY(stopValues.get(0));
stopValueIndex = 0;
return true;
case STATE_EXPANDED:
setTranslationY(0);
stopValueIndex = stopValues.size() - 1;
return true;
case STATE_HIDDEN:
setTranslationY(getHeight());
return true;
}
return false;
}
/**
* Convenience method, uses default duration. See {@link SlidingContainer#animateToState(int, int)}
*/
public boolean animateToState(@SliderState int toState) {
return animateToState(toState, duration);
}
/**
* Animate the container position to a given state. For a non-animated alternative see {@link SlidingContainer#setState(int)}
*/
public boolean animateToState(@SliderState int toState, int duration) {
if (!stopValues.isEmpty()) {
switch (toState) {
case STATE_COLLAPSED:
animate(stopValues.get(0), duration);
stopValueIndex = 0;
return true;
case STATE_EXPANDED:
animate(0, duration);
stopValueIndex = stopValues.size() - 1;
return true;
case STATE_HIDDEN:
animate(getHeight(), duration);
return true;
}
}
return false;
}
protected void notifySlideEvent(float yPosition) {
if (slideListener != null) {
slideListener.onSlide(yPosition);
}
}
public @SliderState int getState() {
int translation = (int) getTranslationY();
if (translation == 0)
return STATE_EXPANDED;
else if (translation == viewHeight)
return STATE_HIDDEN;
else if (stopValues.size() >= 2 && translation == stopValues.get(stopValues.size() - 2))
return STATE_COLLAPSED;
return STATE_COLLAPSED;//check this later
}
@Retention(RetentionPolicy.SOURCE)
@IntDef({STATE_COLLAPSED, STATE_EXPANDED, STATE_HIDDEN})
public @interface SliderState {
}
private static class CustomAnimationListener implements Animator.AnimatorListener {
@Override
public void onAnimationStart(Animator animator) {
}
@Override
public void onAnimationEnd(Animator animator) {
}
@Override
public void onAnimationCancel(Animator animator) {
}
@Override
public void onAnimationRepeat(Animator animator) {
}
}
}